package com.demo.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.concurrent.Callable;

import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.ObjectUtils;

public class RedisCache implements Cache {

	private RedisTemplate<String, Object> redisTemplate;
	private long seconds;

	private String name;

	public RedisTemplate<String, Object> getRedisTemplate() {
		return redisTemplate;
	}

	public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
		this.redisTemplate = redisTemplate;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String getName() {
		return this.name;
	}

	@Override
	public Object getNativeCache() {
		return this.redisTemplate;
	}

	@Override
	public ValueWrapper get(Object key) {
		if (ObjectUtils.isEmpty(key)) return null;
		final byte[] keys = key.toString().getBytes();

		Object object = redisTemplate.execute(new RedisCallback<Object>() {
			public Object doInRedis(RedisConnection connection) throws DataAccessException {
				byte[] values = connection.get(keys);
				return toObject(values);
			}
		});
		return wrap(object);
	}

	@Override
	public void put(Object key, Object value) {
		if (ObjectUtils.isEmpty(key)) return;
		final byte[] keys = key.toString().getBytes();
		final Object val = value;

		redisTemplate.execute(new RedisCallback<Long>() {
			public Long doInRedis(RedisConnection connection) throws DataAccessException {
				byte[] values = toByteArray(val);
				connection.set(keys, values);
				if (seconds > 0) {
					connection.expire(keys, seconds);
				}
				return 1L;
			}
		});
	}

	@Override
	public void evict(Object key) {
		if (ObjectUtils.isEmpty(key)) return;
		final byte[] keys = key.toString().getBytes();

		redisTemplate.execute(new RedisCallback<Long>() {
			public Long doInRedis(RedisConnection connection) throws DataAccessException {
				return connection.del(keys);
			}
		});
	}

	@Override
	public void clear() {
		redisTemplate.execute(new RedisCallback<String>() {
			public String doInRedis(RedisConnection connection) throws DataAccessException {
				connection.flushDb();
				return "ok";
			}
		});
	}

	@Override
	public <T> T get(Object key, Class<T> type) {
		ValueWrapper valueWrapper = get(key);
		return unWrap(valueWrapper, type);
	}

	@Override
	public ValueWrapper putIfAbsent(Object key, Object value) {
		ValueWrapper valueWrapper = get(key);
		if (ObjectUtils.isEmpty(valueWrapper)) {
			valueWrapper = new SimpleValueWrapper(valueWrapper);
			put(key, valueWrapper);
		}
		return valueWrapper;
	}

	@Override
	public <T> T get(Object key, Callable<T> valueLoader) {
		// TODO Auto-generated method stub
		return null;
	}

	// ------------------------------------------------------

	@SuppressWarnings("unchecked")
	private <T> T unWrap(ValueWrapper wrapper, Class<T> type) {
		return wrapper == null ? null : (T) wrapper.get();
	}

	private ValueWrapper wrap(Object object) {
		return object != null ? new SimpleValueWrapper(object) : null;
	}

	private byte[] toByteArray(Object obj) {
		if (obj == null) return null;

		byte[] bytes = null;
		try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
			try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
				oos.writeObject(obj);
				oos.flush();
				bytes = bos.toByteArray();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return bytes;
	}

	private Object toObject(byte[] bytes) {
		if (bytes == null) return null;
		Object obj = null;
		try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes)) {
			try (ObjectInputStream ois = new ObjectInputStream(bis)) {
				obj = ois.readObject();
			}
		} catch (IOException | ClassNotFoundException ex) {
			ex.printStackTrace();
		}
		return obj;
	}

	public long getSeconds() {
		return seconds;
	}

	public void setSeconds(long seconds) {
		this.seconds = seconds;
	}
}